package org.gfix.binding;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gfix.converter.EnumConverter;
import org.gfix.parser.ValidatingValidator;
import org.gfix.types.FixVersion;

/**
 * This feature supports the matching of a stream of FIX events to the a bound class.
 * 
 * It leverages the @FixMessageBindingSet annotations on classes to drive these decisions.
 * 
 * The biggest limitation given that FIX tags may occur in any order is that the end event stream
 * may be reached before a bound class can be chosen. This implementation then returns the 
 * bound class that is fully matched and matched the most contents of the stream. 
 * @author alan
 *
 */
class TypeSelector 
{	
	private FixVersion version;
	private Map<ClassLevelBinding, Matching> possibleClasses = new HashMap<ClassLevelBinding, Matching>();
	private Map<Integer, List<Matching>> bindingsInterestedToTag = new HashMap<Integer, List<Matching>>();
	private Collection<ClassFixBindingDescription> descriptions;
	
	void reset(Collection<ClassFixBindingDescription> descriptions)
	{
		version = null;
		possibleClasses.clear();
		bindingsInterestedToTag.clear();
		this.descriptions = descriptions;
	}
	
	ClassFixBindingDescription newTag(int tag, String contents, List<FixEvent> previous) throws FixBindingException
	{
		switch(tag)
		{
			case ValidatingValidator.FIX_VERSION:
			{
				EnumConverter<FixVersion> ec = EnumConverter.getEnumConverter(FixVersion.class);
				version = ec.convertFromFix(contents);
				setPossibles();
				replayEvents(previous);
				break;
			}
			case ValidatingValidator.FIX_CHECK_SUM:
			{
				// its the last possible message in the item return the most specific of the matches
				// has to be fully matched and then return the most matched
				int mostMatched = 0;
				ClassFixBindingDescription result = null;
				for(Map.Entry<ClassLevelBinding, Matching> me : possibleClasses.entrySet())
				{
					if(me.getValue().isFullyMatched())
					{
						if(mostMatched < me.getValue().tagsMatched)
						{
							result = me.getValue().desc;
							mostMatched = me.getValue().tagsMatched;
						}
					}
				}
				return result;
			}
			default:
			{
				newEvent(tag,contents);
			}
		}
		if(possibleClasses.size() ==0 && version != null)
		{
			throw new FixBindingException("No matching bound classes");
		}
		else if(possibleClasses.size() == 1)
		{
			for(Map.Entry<ClassLevelBinding, Matching> me : possibleClasses.entrySet())
			{
				if(me.getValue().isFullyMatched())
				{
					return me.getValue().desc;
				}
			}
		}
		return null;
	}
	
	private void newEvent(int tag, String contents) {
		if(tag == ValidatingValidator.FIX_VERSION)
		{
			return;
		}
		List<Matching> interestedClbs = bindingsInterestedToTag.get(tag);
		if(interestedClbs != null)
		{
			for(Matching clb: interestedClbs)
			{
				clb.evaluate(tag, contents);
			}
		}
	}

	private void replayEvents(List<FixEvent> previous) {
		for(FixEvent fe:previous)
		{
			newEvent(fe.tag, fe.content);
		}
	}

	private void setPossibles()
	{
		for(ClassFixBindingDescription des : descriptions)
		{
			ClassLevelBinding clb = des.getClassBinding(version);
			if(clb != null)
			{
				Matching match = new Matching(des, clb);
				possibleClasses.put(clb, match);
				for(Integer bint : clb.getBoundTags())
				{
					List<Matching> list = bindingsInterestedToTag.get(bint);
					if(list == null)
					{
						list = new ArrayList<Matching>(3);
						bindingsInterestedToTag.put(bint, list);
					}
					list.add(match);
				}
			}
		}
	}
	
	FixVersion getSelectedVersion()
	{
		return version;
	}
	
	private class Matching
	{
		private ClassFixBindingDescription desc;
		private ClassLevelBinding clb;
		private int tagsToMatch;
		private int tagsMatched;
		
		
		
		Matching(ClassFixBindingDescription desc, ClassLevelBinding clb) {
			this.desc = desc;
			this.clb = clb;
			this.tagsToMatch = clb.getNumberOfboundTags();
		}
		
		private void evaluate(int tag, String contents)
		{
			switch(clb.evaluate(tag, contents))
			{
				case MATCHED:
					tagsMatched++;
				break;
			case NOT_MATCHED:
					possibleClasses.remove(clb);
				break;
			}
		}

		private boolean isFullyMatched()
		{
			return tagsToMatch <= tagsMatched;
		}

		
		
	}
}
